# Retrieval of tags
# by Cee Pee

import forumapi
import re
from deps.Form import Form

__all__ = [
	# Utilities
	"get_tag_cloud", "get_tag_suggest",
	
	# Extensible objects
	"Tag", "ThreadTags",
]

## Utilities ##

def get_tag_cloud(session):
	"""Given a `Session`, returns a list of tuples containing a `Tag` object
	and relative level integer, with 1 denoting the tags used the least of 
	those shown, as fetched from the tag cloud page. Only the most used tags
	are shown on this page."""
	
	tags = []
	
	page = session.get_page("tags.php")
	
	page_soup = session.soup(page)
	
	tags_soup = page_soup.findAll("a", {"class": lambda classes: classes and "tagcloudlink" in classes})
	for tag in tags_soup:
		level = int(re.match("tagcloudlink level(\d+)", tag["class"]).group(1))
		tags.append((Tag(session, name=str(tag.string)), level))
	
	return(tags)

def get_tag_suggest(session, fragment):
	"""Given a `Session` and a tag fragment, returns a list of `Tag` objects
	that start with the fragment.
	
	Could also be used to check whether a tag exists, though `Tag.threads`
	is probably more useful for this, and yields more data."""
	
	tags = []
	
	ajax_data = {
		"securitytoken": session.get_security_token(),
		"do": "tagsearch",
		"fragment": fragment,
	}
	
	ajax_page = session.get_page("ajax.php?do=usersearch", post_data=ajax_data)
	
	xml_soup = session.soup(ajax_page, "xml")
	tag_soup = xml_soup.findAll("tag")
	
	for tag in tag_soup:
		tags.append(Tag(session, name=str(tag.string)))
	
	return(tags)

## Hooks ##

def _get_search_results_page(a=None, kw=None, search_data=None, **kwargs):
	if list(kw.keys()) == ["tag"] and len(a) == 0:
		search_data["tag"] = kw["tag"]
		return("tags.php")

def _get_search_results(kw=None, search_data=None, session=None, **kwargs):
	if "thread" not in kw:
		if "tag" in kw:
			search_data["tag"] = Tag.obj(session, kw["tag"]).name

def _get_threads_list(cells=None, status_info=None, deleted=None, thread=None, session=None, **kwargs):
	if not(status_info["moved"] or deleted):
		tag_icon = cells[2].find("img", src=lambda url: url and "tag.png" in url)
		if tag_icon:
			tags_list = tag_icon["alt"].split(",")
			thread._tags = [Tag(session, name=tag.strip()) for tag in tags_list]
		else:
			thread._tags = []

def _retrieve_metadata(session=None, soup=None, loc=None, t=None, **kwargs):
	if loc == "thread":
		t._tags = ThreadTags(session, t)
		t._tags.retrieve_tags(soup=soup, loc="thread")

def _prepare_thread(session=None, kw=None, post_data=None, **kwargs):
	if "tags" in kw and "taglist" in post_data:
		post_data["taglist"] = ",".join([Tag.obj(session, tag).name for tag in kw["tags"]])

def _prepared_post_submit_thread(preparedpost=None, thread=None, **kwargs):
	if "tags" in preparedpost.kw:
		thread._tags = preparedpost.kw["tags"]

forumapi.hooks.add("vb_get_search_results_page", _get_search_results_page)
forumapi.hooks.add("vb_get_search_results", _get_search_results)
forumapi.hooks.add("vb_get_threads_list", _get_threads_list)
forumapi.hooks.add("vb_retrieve_metadata", _retrieve_metadata)
forumapi.hooks.add("vb_prepare_thread", _prepare_thread)
forumapi.hooks.add("vb_prepared_post_submit_thread", _prepared_post_submit_thread)

## Main classes ##

class Tag(forumapi.ExtensibleObject):
	"""A thread tag. It has a name and threads associated with it."""
	
	@classmethod
	def obj(cls, session, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], int):
			return(cls(session, *a[1:], id=a[0], **kw))
		elif isinstance(a[0], str):
			return(cls(session, *a[1:], name=a[0], **kw))
		else:
			return(None)
	
	def __init__(self, session, name=None, id=None, threads=None):
		if not (name or id):
			raise(ValueError("Name or ID required."))
		
		self.session = session
		
		self._name = name
		self._id = id
		
		if isinstance(threads, list):
			self._threads = [self.module.get_attr("Thread").obj(session, thread) for thread in threads]
		else:
			self._threads = threads
		
		self._find_instances()
	
	def _merge(self, other):
		if self._name:
			other._id = self._name
		if self._id:
			other._id = self._id
		
		self._merge_obj(other, "_threads")
	
	@property
	def data(self):
		return({
			"name": self._name,
			"id": self._id,
			"threads": forumapi._flatten_data(self._threads),
		})
	
	@property
	def id(self):
		if self._id:
			return(self._id)
		else:
			thread_tags = self.threads[0].tags
			thread_tags.retrieve_tags(loc="manage")
			
			for tag in thread_tags:
				if self == tag:
					return(self._id)
			
			raise(forumapi.UnknownError("Cannot retrieve tag ID."))
	
	@property
	def name(self):
		if self._name:
			return(self._name)
		else:
			raise(NotImplementedException("Cannot retrieve tag name."))
	
	@property
	def threads(self):
		if self._threads is not None:
			return(self._threads)
		else:
			self.retrieve()
			return(self._threads)
	
	def retrieve(self, pages=None, start=1, t=None):
		"""Obtains the threads that are tagged with the tag.
		
		By default only retrieves the first page of the results; more pages can
		be retrieved using the `pages` parameter or a `t` parameter representing
		a Thread that is to be found."""
		
		if self._threads is None:
			if pages is None:
				pages = 1
			self._threads = self.session.get_search_results(tag=self.name, pages=pages, start=start, t=t)
		else:
			self._threads.get_pages(pages=pages, start=start, t=t)
	
	def __eq__(self, other):
		if isinstance(other, Tag):
			if self._name and other._name:
				return(self.name == other.name)
			else:
				return(self.id == other.id)
		elif isinstance(other, str):
			return(self.name == other)
		elif isinstance(other, int):
			return(self.id == other)
		else:
			return(False)
	
	def __int__(self):
		return(self.id)
	
	def __str__(self):
		return(self.name)
	
	def __repr__(self):
		return("<vbulletin.Tag {0}\"{self.name}\">".format("{self.id} ".format(self=self) if self._id else "", self=self))

class ThreadTags(forumapi.ExtensibleObject):
	"""Object representing the tags in a specific thread, and their associated
	metadata. Aside from being an object pretending to be a (supposedly)
	immutable list of Tag objects, this object also contains the tag form and
	information retrieved from it: which tags were added by whom and which can
	be edited by the session user?"""
	
	@classmethod
	def obj(cls, thread, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], list):
			return(cls(thread, *a[1:], tags=a[0], **kw))
		else:
			return(None)
	
	def __init__(self, thread, tags=None):
		self.session = thread.session
		self.thread = thread
		
		if isinstance(tags, list):
			self._tags = [Tag.obj(self.session, tag, threads=[self.thread]) for tag in tags]
		else:
			self._tags = None
		
		self._tags_info = None
		self._tags_form = None
		
		self._find_instances()
	
	def _merge(self, other):
		# Only tags may be set at this time
		self._merge_list(other, "_tags")
	
	@property
	def data(self):
		if self._tags is not None:
			return([str(tag) for tag in self.tags])
		else:
			return(None)
	
	@property
	def tags(self):
		if self._tags is not None:
			return(self._tags)
		else:
			self.retrieve_tags()
			return(self._tags)
	
	@property
	def tags_info(self):
		if self._tags_info is not None:
			return(self._tags_info)
		else:
			self.retrieve_tags(loc="manage")
			return(self._tags_info)
	
	@property
	def tags_form(self):
		if self._tags_form is not None:
			return(self._tags_form)
		else:
			self.retrieve_tags(loc="manage")
			if self._tags_form == False:
				raise(forumapi.PermissionError("Thread has no tags form."))
			
			return(self._tags_form)
	
	_tag_addedby_pattern = None
	
	@property
	def tag_addedby_pattern(self):
		if self._tag_addedby_pattern:
			return(self._tag_addedby_pattern)
		else:
			self.__class__._tag_addedby_pattern = re.compile(self.session.lang.phrases["tag_addedby"])
			return(self._tag_addedby_pattern)
	
	def retrieve_tags(self, soup=None, loc=None):
		"""Given a `BeautifulSoup` of a thread page, retrieves the tags for
		the thread. The `loc` param can be either "thread" for a normal thread
		page, or "manage" for a tags manage page."""
		
		if soup is None:
			if self.session.guest or loc == "thread":
				page = self.session.get_page("showthread.php?t={0}".format(self.thread.id))
				loc = "thread"
			else:
				page = self.session.get_page("threadtag.php?t={0}".format(self.thread.id))
				loc = "manage"
			
			soup = self.session.soup(page)
		
		# TODO: Move this to somewhere before the page is retrieved
		if self._tags is not None and loc == "thread":
			return(None)
		
		tags_soup = None
		if loc == "thread":
			tags_soup = soup.find("td", id="tag_list_cell")
			
			if not tags_soup:
				return(None)
			
			self._tags = [Tag(self.session, name=str(tag.string)) for tag in tags_soup.findAll("a")]
		elif loc == "manage":
			self._tags = []
			self._tags_info = {}
			
			form_soup = soup.find("form", action=lambda url: "threadtag.php" in url)
			
			if not form_soup:
				self._tags_form = False
				
				return(None)
			
			self._tags_form = Form(form_soup, session=self.session)
			
			for tag_control in self._tags_form.get_all("input", "checkbox"):
				user = None
				editable = False
				
				attrs = dict(tag_control.soup.parent.parent.attrs)
				
				if attrs["class"] == "tag mine":
					user = self.session.user
					editable = True
				elif "title" in attrs:
					# Mods can view who added, and edit
					match = self.tag_addedby_pattern.match(attrs["title"])
					
					if match:
						user = User(self.session, name=match.group(1))
						editable = True
				elif "disabled" not in tag_control.attrs:
					# We must be the thread starter...
					editable = True
				
				# Initial state is checked so the value should be set
				id = int(tag_control.value)
				name = str(tag_control.label)
				
				self._tags_info[name] = {
					"adder": user,
					"editable": editable,
				}
				
				self._tags.append(Tag(self.session, name=name, id=id))
	
	def update_tags(self, add=None, remove=None):
		"""Given lists of `Tag` objects or tag names to add and remove,
		updates the tags for this thread."""
		
		if not (add or remove):
			# Nothing to add or remove!
			return(None)
		
		if not isinstance(add, list):
			add = [add]
		if not isinstance(remove, list):
			remove = [remove]
		
		if (add and not isinstance(add, (list, tuple))) or (remove and not isinstance(remove, (list, tuple))):
			raise(ValueError("Expecting lists or tuples."))
		
		if not self.tags_form:
			raise(forumapi.UnknownError("Thread has no tags form."))
		
		## Adding tags
		add_tags = [Tag.obj(self.session, tag).name for tag in add]
		
		if "taglist" in self.tags_form:
			self.tags_form["taglist"] = ",".join(add_tags)
		else:
			raise(forumapi.PermissionError("Cannot add tags to thread."))
		
		## Removing tags
		remove_tags = [Tag.obj(self.session, tag).name for tag in remove]
		
		for tag in self.tags:
			if tag in remove_tags:
				# Only remove tags that are editable
				if tag.name not in self._tags_info or self._tags_info[tag.name]["editable"]:
					self.tags_form.get("tag_{0}".format(tag.id)).set()
		
		self.tags_form.submit()
	
	def __eq__(self):
		if isinstance(other, ThreadTags):
			return(self.thread == other.thread)
		elif isinstance(other, list):
			return(self.tags == other)
		else:
			return(False)
	
	def __getitem__(self, key):
		return(self.tags[key])
	
	def __contains__(self, tag):
		return(tag in self.tags)
	
	def __len__(self):
		return(len(self.tags))
	
	def __iter__(self):
		return(iter(self.tags))
	
	def __reversed__(self):
		return(reversed(self.tags))
	
	def __repr__(self):
		return("ThreadTags(thread={self.thread.id}, {tags})".format(self=self, tags=repr(self.tags)))
